package emulator.assembler;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Observable;

import emulator.hardware.HwWord;

public class SymbolTable extends Observable
{
	public static SymbolTableSource userSymbol = new SymbolTableSource("User"); 
	private HashMap<Integer, SymbolTableEntry> table = new HashMap<Integer, SymbolTableEntry>();
	private HashMap<String, SymbolTableEntry> stable = new HashMap<String, SymbolTableEntry>();
	private HashSet<Integer> jump_target_set = new HashSet<Integer>();
	private static int max_label_distance = 125;	// Branch distance (backwards)
	private static int max_zp_label_distance = 2; 	// Distance for self-modifying code
	
	public synchronized Collection<SymbolTableEntry> getSymbols()
	{
		return table.values();
	}
	
	public synchronized String getJumpLabel(int address)
	{
		String label = getSymbol(address);
		if (label == null && jump_target_set.contains(address))
			label = buildJumpLabelFromAddress(address);
		return label;
	}

	public synchronized String getLabelOrAddress(int address) {
		String label = getSymbol(address);
		return (label == null ? buildSymbolFromAddress(address) : label);
	}

	public synchronized String createJumpLabel(int address) {
		jump_target_set.add(address);
		return getJumpLabel(address);
	}

	public synchronized SymbolTableEntry getSymbolEntry(int address) {
		return table.get(address);
	}

	public synchronized String getRemark(int address) {
		SymbolTableEntry entry = getSymbolEntry(address);
		return (entry == null ? null : entry.getRemark());
	}

	private synchronized String getSymbol(int address) {
		SymbolTableEntry entry = getSymbolEntry(address);
		return (entry == null ? null : entry.getLabel()); 
	}

	public synchronized void load(InputStream input_stream, String sourceName)
	{
		SymbolTableSource symbolSource = new SymbolTableSource(sourceName);
		InputStreamReader reader = new InputStreamReader(input_stream);
	    BufferedReader in = new BufferedReader(reader);
	    String line = null;
	    try {
			while ((line = in.readLine()) != null)
			{
				SymbolTableEntry entry = SymbolTableEntry.createFromString(line);
				if (entry != null)
				{
					entry.setSource(symbolSource);
					table.put(entry.getAddress(), entry);
					if (entry.getLabel() != null)
						stable.put(entry.getLabel().toUpperCase(), entry);
				}
			}
		} catch (IOException e) {
		}
	    setChanged();
	    notifyObservers();
	}

	public synchronized void addSymbol(int address, String label, String remark, int type)
	{
		SymbolTableEntry entry = new SymbolTableEntry();
		entry.setAddress(address);
		entry.setLabel(label);
		entry.setRemark(remark);
		entry.setType(type);
		entry.setSource(userSymbol);
		table.put(address, entry);
		if (label != null)
			stable.put(label.toUpperCase(), entry);
	}

	private String buildJumpLabelFromAddress(int address) {
		return "L_"+new HwWord(address);
	}

	private String buildSymbolFromAddress(int address) {
		SymbolTableEntry entry = getSymbolEntry(address);
		int max_i = Math.min((address < 0x0100 ? max_zp_label_distance : max_label_distance),
							  address);
		int off = 0;
		for (int i = 1; entry == null && i <= max_i; i++)
		{
			entry = table.get(address - i);
			off = i;
		}
		if (entry != null)
		{
			if (entry.getLabel() != null)
				return (off == 0 ? entry.getLabel() : entry.getLabel() + "+" + Integer.toString(off)); 
		}
		return "$"+new HwWord(address);
	}

	public synchronized LinkedList<Integer> getCodeLabels() {
		LinkedList<Integer> code_labels = new LinkedList<Integer>();
		for (SymbolTableEntry entry : table.values())
		{
			if (entry.getType() == SymbolTableEntry.SYMBOL_TYPE_CODE)
				code_labels.push(entry.getAddress());
		}
		return code_labels;
	}

	public synchronized HashSet<Integer> getJumpTargets() {
		 return jump_target_set;		
	}

	public synchronized int findLabel(String search_text) 
	{
		SymbolTableEntry entry = stable.get(search_text.toUpperCase());
		return (entry != null ? entry.getAddress() : -1);
	}

	public void dump(PrintStream out)
	{
		for (SymbolTableEntry entry : table.values())
		{
			entry.dump(out);
			out.println();
		}
	}

	public void save(OutputStream file, Collection<SymbolTableSource> selectedSources)
	{
		PrintStream out = new PrintStream(file);
		out.print(
				";\n" +
				"; Symbols written by vic20emu (" + new Date() + ")\n" +
				";\n" +
				"; Line format:\n" +
				";\n" +
				"; <Address> <Label> <Remark> <Data type>\n" +
				";\n" +
				"; Data types :\n" +
				";\n" +
				";       DATA            Misc. data\n" +
				";       TEXT            String terminated with 00\n" +
				";       WORD            Vectors in LO/HI byte pairs\n" +
				";       CHIP            I/O Area\n" +
				";       EMPTY           ROM containing FF's or AA's\n" +
				";\n\n");
		HashSet<SymbolTableSource> sources = new HashSet<SymbolTableSource>(selectedSources);
		for (SymbolTableEntry entry : getSortedSymbols())
		{
			if (sources.contains(entry.getSource()))
			{
				entry.writeLine(out);
			}
		}
		out.flush();
	}

	public Collection<SymbolTableEntry> getSortedSymbols()
	{
		ArrayList<SymbolTableEntry> sortedSymbols = new ArrayList<SymbolTableEntry>(getSymbols());
		Collections.sort(sortedSymbols, new Comparator<SymbolTableEntry>() {
			@Override
			public int compare(SymbolTableEntry o1, SymbolTableEntry o2) {
				return o1.getAddress() - o2.getAddress();
			}
			
		});
		return sortedSymbols;
	}

	public void delete(Collection<SymbolTableSource> selectedSources)
	{
		HashSet<SymbolTableSource> sources = new HashSet<SymbolTableSource>(selectedSources);
		for (SymbolTableEntry entry : new ArrayList<SymbolTableEntry>(getSymbols()))
		{
			if (sources.contains(entry.getSource()))
			{
				table.remove(entry.getAddress());
			}
		}
		rebuildLabelIndex();
	    setChanged();
	    notifyObservers();
	}

	private void rebuildLabelIndex()
	{
		for (SymbolTableEntry entry : getSymbols())
		{
			if (entry.getLabel() != null)
				stable.put(entry.getLabel().toUpperCase(), entry);
		}
	}
}
